// (c) Copyright 2013-2014 Stephen Gold <sgold@sonic.net>
// Distributed under the terms of the GNU General Public License

/*
 This file is part of the Bats Game.

 The Bats Game is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by the
 Free Software Foundation, either version 3 of the License, or (at your
 option) any later version.

 The Bats Game is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 for more details.

 You should have received a copy of the GNU General Public License
 along with the Bats Game.  If not, see <http://www.gnu.org/licenses/>.
 */
package bats;

import bats.control.BatsControl;
import bats.control.PickedControl;
import bats.control.RevealControl;
import bats.control.physics.BurnControl;
import bats.unit.AggressiveControl;
import bats.unit.TargetControl;
import bats.unit.ViewpointControl;
import com.jme3.bullet.collision.PhysicsCollisionObject;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import java.util.logging.Level;
import java.util.logging.Logger;
import jme3utilities.MyString;
import jme3utilities.NameGenerator;

/**
 * Utility methods for accessing game-specific properties of nodes and spatials,
 * most of which are stored in user data. Aside from test cases, all methods
 * here should be public and static.
 *
 * @author Stephen Gold <sgold@sonic.net>
 */
public class SpatialProperties {
    // *************************************************************************
    // constants

    /**
     * hardness setting for dirt (in meters/second)
     */
    final private static float dirtHardness = 1f;
    /**
     * hardness setting for stone (in meters/second)
     */
    final private static float stoneHardness = 100f;
    /**
     * hardness setting for soft wood (in meters/second)
     */
    final private static float woodHardness = 1f;
    /**
     * message logger for this class
     */
    final private static Logger logger =
            Logger.getLogger(SpatialProperties.class.getName());
    /**
     * user data key for ammo
     */
    final private static String ammoKey = "ammo";
    /**
     * user data key for hardness
     */
    final private static String hardnessKey = "hardness";
    /**
     * user data key for launcher name
     */
    final private static String launcherNameKey = "launcherName";
    /**
     * user data key for pick type
     */
    final private static String pickTypeKey = "pickType";
    /**
     * user data key for viewpoint tilt angle
     */
    final private static String tiltAngleKey = "tiltAngle";
    /**
     * user data key for topography
     */
    final private static String topographyKey = "topography";
    /**
     * user data key for topY
     */
    final private static String topYKey = "topY";
    /**
     * user data key for viewpoint turn angle
     */
    final private static String turnAngleKey = "turnAngle";
    // *************************************************************************
    // constructors

    /**
     * A private constructor to inhibit instantiation of this class.
     */
    private SpatialProperties() {
    }
    // *************************************************************************
    // new methods exposed

    /**
     * Test whether a unit's targets are visible.
     *
     * @param spatial which unit (not null)
     * @return true if the unit's targets are visible, false if they're hidden
     */
    public static boolean areTargetsVisible(Spatial spatial) {
        TargetControl targetControl = spatial.getControl(TargetControl.class);
        if (targetControl == null) {
            return false;
        }
        boolean result = targetControl.isEnabled();
        return result;
    }

    /**
     * Clear a unit's pick type.
     *
     * @param spatial which unit to alter (not null)
     */
    public static void clearPickType(Spatial spatial) {
        assert spatial != null;
        setPickType(spatial, null);
    }

    /**
     * If a launcher's ammunition supply is finite, reduce it by one.
     *
     * @param spatial which launcher (not null)
     */
    public static void consumeAmmo(Spatial spatial) {
        Integer ammo = spatial.getUserData(ammoKey);
        if (ammo != null) {
            ammo--;
        }
    }

    /**
     * Find a spatial's pickType by searching up the scene graph.
     *
     * @param spatial starting point for the search
     * @return a spatial with pickType set, or null if none found
     */
    public static Spatial findPickable(Spatial spatial) {
        Spatial current = spatial;
        while (current != null) {
            if (hasPickType(current)) {
                return current;
            }
            current = current.getParent();
        }
        return null;
    }

    /**
     * Find a spatial with an enabled physics control by searching up the scene
     * graph.
     *
     * @param spatial starting point for the search
     * @return a spatial with an enabled physics control, or null if none found
     */
    public static Spatial findResistor(Spatial spatial) {
        Spatial current = spatial;
        while (current != null) {
            if (isResistor(current)) {
                return current;
            }
            current = current.getParent();
        }
        return null;
    }

    /**
     * Determine an obstacle's hardness.
     *
     * @param spatial which obstacle (not null)
     * @return missile velocity for penetration (in meters/second, &ge;0)
     */
    public static float getHardness(Spatial spatial) {
        Float hardness = spatial.getUserData(hardnessKey);
        assert hardness != null : spatial.getName();
        return hardness;
    }

    /**
     * Determine a spatial's pickType.
     *
     * @param spatial which spatial to analyze (not null)
     * @return "block", "bob", "oto", or null
     */
    public static String getPickType(Spatial spatial) {
        String result = spatial.getUserData(pickTypeKey);
        validatePickType(result);
        return result;
    }

    /**
     * Determine a spatial's prefix type.
     *
     * @param spatial which spatial to analyze (not null)
     * @return the type-string
     */
    public static String getPrefixType(Spatial spatial) {
        String name = spatial.getName();
        String prefixType = NameGenerator.getPrefix(name);
        return prefixType;
    }

    /**
     * Access a spatial's viewpoint tilt angle.
     *
     * @param spatial which spatial (not null)
     * @return viewpoint tilt angle (in radians, forward is positive)
     */
    public static float getTiltAngle(Spatial spatial) {
        Float tiltAngle = spatial.getUserData(tiltAngleKey);
        assert tiltAngle != null;
        return tiltAngle;
    }

    /**
     * Access a spatial's topY.
     *
     * @param spatial which spatial (not null)
     * @return its highest possible Y-coordinate
     */
    public static float getTopY(Spatial spatial) {
        Float topY = spatial.getUserData(topYKey);
        assert topY != null;
        return topY;
    }

    /**
     * Access a terrain spatial's topography.
     *
     * @param spatial which terrain (not null)
     * @return the name of the topography (not null)
     */
    public static String getTopography(Spatial spatial) {
        String topography = spatial.getUserData(topographyKey);
        assert topography != null;
        return topography;
    }

    /**
     * Access a spatial's viewpoint turn angle.
     *
     * @param spatial which spatial (not null)
     * @return viewpoint turn angle (in radians, left is positive)
     */
    public static float getTurnAngle(Spatial spatial) {
        Float turnAngle = spatial.getUserData(turnAngleKey);
        assert turnAngle != null;
        return turnAngle;
    }

    /**
     * Test whether a launcher has any ammunition left.
     *
     * @param spatial which launcher (not null)
     * @return true if there's more ammo, false if the unit has run out
     */
    public static boolean hasMoreAmmo(Spatial spatial) {
        Integer ammo = spatial.getUserData(ammoKey);
        boolean result = ammo == null || ammo > 0;
        return result;
    }

    /**
     * Test whether a given spatial has a pick type.
     *
     * @param spatial which spatial to test (not null)
     * @return true if the spatial's pick type is non-null, false if it's null
     */
    public static boolean hasPickType(Spatial spatial) {
        assert spatial != null;

        String pickType = getPickType(spatial);
        boolean result = pickType != null;
        return result;
    }

    /**
     * Test whether a spatial represents a map block.
     *
     * @param spatial which spatial to test (not null)
     * @return true if it represents a map block, false otherwise
     */
    public static boolean isBlock(Spatial spatial) {
        assert spatial != null;

        String pickType = getPickType(spatial);
        boolean result = "block".equals(pickType);
        return result;
    }

    /**
     * Test whether a spatial is a flame or not.
     *
     * @param spatial which spatial (not null)
     * @return true if it's a flame, false if it's not
     */
    public static boolean isFlame(Spatial spatial) {
        BurnControl burnControl = spatial.getControl(BurnControl.class);
        boolean result = burnControl != null;
        return result;
    }

    /**
     * Test whether a spatial is hidden or not.
     *
     * @param spatial which spatial (not null)
     * @return true if hidden, false if not hidden
     */
    public static boolean isHidden(Spatial spatial) {
        RevealControl control = spatial.getControl(RevealControl.class);
        if (control == null) {
            return false;
        } else {
            return !control.isEnabled();
        }
    }

    /**
     * Test whether a given unit launched a given missile.
     *
     * @param launcher which unit to test (not null)
     * @param missile which missile to test (not null)
     * @return true if the unit launched the missile, otherwise false
     */
    public static boolean isLauncher(Spatial launcher, Spatial missile) {
        String launcherName = missile.getUserData(launcherNameKey);
        if (launcherName == null) {
            return false;
        }
        String name = launcher.getName();
        boolean result = launcherName.equals(name);
        return result;
    }

    /**
     * Test whether a spatial is picked or not.
     *
     * @param spatial which spatial (not null)
     * @return true if it's picked, false if it's not picked
     */
    public static boolean isPicked(Spatial spatial) {
        PickedControl pickedControl = spatial.getControl(PickedControl.class);
        if (pickedControl == null) {
            return false;
        }
        boolean result = pickedControl.isEnabled();
        return result;
    }

    /**
     * Test whether a spatial represents a prop.
     *
     * @param spatial which spatial to test (not null)
     * @return true if it represents a prop, false otherwise
     */
    public static boolean isProp(Spatial spatial) {
        assert spatial != null;

        String pickType = getPickType(spatial);
        boolean result = "prop".equals(pickType);
        return result;
    }

    /**
     * Test whether the specified spatial has a collision object.
     *
     * @param spatial which spatial to test (not null)
     * @return true if the spatial has a collision object, otherwise false
     */
    public static boolean isResistor(Spatial spatial) {
        int count = spatial.getNumControls();
        for (int i = 0; i < count; i++) {
            Object object = spatial.getControl(i);
            if (object instanceof PhysicsCollisionObject) {
                boolean isEnabled = BatsControl.isEnabled(object);
                if (isEnabled) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Test whether a spatial represents a unit.
     *
     * @param spatial which spatial to test (not null)
     * @return true if it's a unit, false otherwise
     */
    public static boolean isUnit(Spatial spatial) {
        String name = spatial.getName();
        boolean result = NameGenerator.isFrom(name, "bob")
                || NameGenerator.isFrom(name, "oto")
                || NameGenerator.isFrom(name, "sinbad");
        return result;
    }

    /**
     * Test whether a unit's viewpoint is visible.
     *
     * @param spatial which unit (not null)
     * @return true if the unit's viewpoint is visible, false if it's hidden
     */
    public static boolean isViewpointVisible(Spatial spatial) {
        ViewpointControl control = spatial.getControl(ViewpointControl.class);
        if (control == null) {
            return false;
        }
        boolean result = control.isEnabled();
        return result;
    }

    /**
     * Reveal a hidden spatial.
     *
     * @param spatial which spatial to reveal (not null)
     */
    public static void reveal(Spatial spatial) {
        RevealControl control = spatial.getControl(RevealControl.class);
        control.setEnabled(true);
    }

    /**
     * Alter an object's hardness. NOTE: recursive!
     *
     * @param spatial which object to alter (not null)
     * @param newValue normal velocity for missile penetration (in
     * meters/second, &ge;0)
     */
    public static void setHardness(Spatial spatial, float newValue) {
        assert newValue >= 0f : newValue;

        if (spatial instanceof Geometry) {
            spatial.setUserData(hardnessKey, newValue);
            return;
        }
        assert spatial instanceof Node;
        Node node = (Node) spatial;
        for (Spatial child : node.getChildren()) {
            setHardness(child, newValue);
        }
    }

    /**
     * Alter an object's hardness based on its material.
     *
     * @param spatial which object to alter (not null)
     * @param materialName name of the material (not null)
     */
    public static void setHardness(Spatial spatial, String materialName) {
        float hardness;
        switch (materialName) {
            case "dirt":
                hardness = dirtHardness;
                break;
            case "bronze":
            case "pavers":
            case "stone":
            case "tile":
                hardness = stoneHardness;
                break;
            case "wood":
                hardness = woodHardness;
                break;
            default:
                logger.log(Level.SEVERE, "Unknown material {0}",
                        MyString.quote(materialName));
                assert false;
                hardness = stoneHardness;
                break;
        }

        setHardness(spatial, hardness);
    }

    /**
     * Initialize a missile's launcher.
     *
     * @param missile which missile (not null)
     * @param launcher which launcher launched the missile (not null)
     */
    public static void setLauncher(Spatial missile, Spatial launcher) {
        assert missile.getUserData(launcherNameKey) == null;
        String name = launcher.getName();
        missile.setUserData(launcherNameKey, name);
    }

    /**
     * Alter a spatial's pick type.
     *
     * @param spatial which spatial to alter (not null)
     * @param newValue
     */
    public static void setPickType(Spatial spatial, String newValue) {
        validatePickType(newValue);
        spatial.setUserData(pickTypeKey, newValue);
    }

    /**
     * Alter the pick status of a spatial.
     *
     * @param spatial which spatial (not null)
     * @param newState true to pick, false to unpick
     */
    public static void setPicked(Spatial spatial, boolean newState) {
        PickedControl pickedControl = spatial.getControl(PickedControl.class);
        if (pickedControl != null) {
            pickedControl.setEnabled(newState);
        }
    }

    /**
     * Alter the visibility of a unit's targets.
     *
     * @param spatial which unit (not null)
     * @param newState true to make the unit's targets visible, false to hide
     * them
     */
    public static void setTargetsVisible(Spatial spatial, boolean newState) {
        TargetControl control = spatial.getControl(TargetControl.class);
        if (control != null) {
            control.setEnabled(newState);
        }
    }

    /**
     * Alter the visibility of a unit's viewpoint.
     *
     * @param spatial which unit (not null)
     * @param newState true to make the unit's viewpoint visible, false to hide
     * it
     */
    public static void setViewpointVisible(Spatial spatial, boolean newState) {
        ViewpointControl control = spatial.getControl(ViewpointControl.class);
        if (control != null) {
            control.setEnabled(newState);
        }
    }

    /**
     * Alter the tilt angle of a spatial's viewpoint.
     *
     * @param spatial which spatial to alter (not null)
     * @param newValue (in radians, left is positive)
     */
    public static void setTiltAngle(Spatial spatial, float newValue) {
        spatial.setUserData(tiltAngleKey, newValue);
    }

    /**
     * Alter the topography of a terrain spatial.
     *
     * @param spatial which spatial (not null)
     * @param newValue (not null)
     */
    public static void setTopography(Spatial spatial, String newValue) {
        assert newValue != null;

        spatial.setUserData(topographyKey, newValue);
    }

    /**
     * Alter the top Y-coordinate of a spatial.
     *
     * @param spatial which spatial to alter (not null)
     * @param newValue
     */
    public static void setTopY(Spatial spatial, float newValue) {
        spatial.setUserData(topYKey, newValue);
    }

    /**
     * Alter the turn angle of a spatial's viewpoint.
     *
     * @param spatial which spatial to alter (not null)
     * @param newValue (in radians, left is positive)
     */
    public static void setTurnAngle(Spatial spatial, float newValue) {
        spatial.setUserData(turnAngleKey, newValue);
    }

    /**
     * Toggle the aggressive state of a unit.
     *
     * @param spatial which unit to toggle (not null)
     */
    public static void toggleAggressive(Spatial spatial) {
        AggressiveControl aggressiveControl =
                spatial.getControl(AggressiveControl.class);
        if (aggressiveControl != null) {
            aggressiveControl.toggleEnabled();
        }
    }

    /**
     * Toggle a spatial in or out of the pick.
     *
     * @param spatial which spatial (not null)
     */
    public static void togglePick(Spatial spatial) {
        assert spatial != null;

        boolean isPicked = !isPicked(spatial);
        setPicked(spatial, isPicked);
    }

    /**
     * Assert that a pick type is one of the known values.
     *
     * @param pickType one of the values listed below, or null:
     *
     * "block", "bob", "oto", "prop"
     */
    public static void validatePickType(String pickType) {
        assert pickType == null
                || "block".equals(pickType)
                || "bob".equals(pickType)
                || "oto".equals(pickType)
                || "prop".equals(pickType)
                || "sinbad".equals(pickType) : pickType;
    }

    /**
     * Assert that a userData key is one of the known keys.
     *
     * @param key one of the key values listed below:<pre>
     *
     * key value     where found  type of datum  main use
     * ============  ===========  =============  ================
     * AttachedBone  attachnodes  Bone
     * ammo          units        Integer        LaunchControl.hasMoreAmmo()
     * hardness      various      Float          getHardness()
     * launcherName  missile      String
     * pickType      various      String         findPickable()
     * tiltAngle     otos         Float
     * topography    terrain      String
     * topY          various      float          WorldMap.writeXml()
     * turnAngle     otos         Float
     * </pre>
     *
     * Best practice is to reserve user data for custom attributes!
     */
    public static void validateUserDataKey(String key) {
        assert "AttachedBone".equals(key)
                || ammoKey.equals(key)
                || hardnessKey.equals(key)
                || launcherNameKey.equals(key)
                || pickTypeKey.equals(key)
                || tiltAngleKey.equals(key)
                || topographyKey.equals(key)
                || topYKey.equals(key)
                || turnAngleKey.equals(key) : key;
    }
}